/*
 * GEmilErpelRoundRobinMoveMaker.java
 * Created on 4 janv. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.seasoncycle.movemaking.impl;


import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.contest.GRoundRobinContestElement;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.standings.GStandings;
import com.hockeo.vhbase.data.helper.GMatchType;
import com.hockeo.vhbase.data.helper.GStandingsType;
import com.hockeo.vhbase.error.GException;
import com.hockeo.vhbase.seasoncycle.movemaking.GAbstractMoveMaker;

/**
 * This class is responsible for making moves for computer controlled clubs that rely on
 * the EmilErpel user implementation. It only handles round robin style contest elements.
 *
 * @version $Id$
 * @author jjanke
 */
public class GEmilErpelRoundRobinMoveMaker extends GAbstractMoveMaker
{
  // Calculated data (once before goals are placed for the first match)
  //
  protected GRoundRobinContestElement                   d_elementRR;
  protected Map<GStandingsType, GStandings>             d_mapStandings;
  protected Map<GClub, Map<GStandingsType, GStandings>> d_mapAdversaryStandings;

  // Values change for every match
  //
  protected BigDecimal                                  d_decAvailableGoals;
  protected int[]                                       d_anGoalProbabilities;
  protected int                                         d_nMaxGoalsForCurrentMatch;
  protected int                                         d_nNumRemainingTotalMatches;
  protected int                                         d_nNumRemainingHomeMatches;
  protected int                                         d_nNumRemainingAwayMatches;
  protected MatchResultRecommendation                   d_resultRecommendation;

  @Override
  public void setup() throws GException
  {
    d_elementRR = getDao().findEntity( GRoundRobinContestElement.class, getElement().getId() );
    d_mapStandings = getDao().findCurrentStandings( getClub(), d_elementRR );

    // If there is more than one match to play with the current goal account, we
    // load the standings of the adverse teams.
    //
    if ( getMovedayStatus() != MovedayStatus.LAST_OF_SEASON || getMatches().size() > 1 )
    {
      d_mapAdversaryStandings = new HashMap<GClub, Map<GStandingsType, GStandings>>();
      for ( GMatch match : getMatches() )
      {
        GClub clubAdverse = match.getOpponent( getClub() );

        if ( !d_mapAdversaryStandings.containsKey( clubAdverse ) )
          d_mapAdversaryStandings.put( clubAdverse, getDao().findCurrentStandings( clubAdverse, d_elementRR ) );
      }
    }
    else
      d_mapAdversaryStandings = null;
  }

  @Override
  public void computeMoves() throws GException
  {
    // Load available amount of goals for first match (must be recalculated
    // after each match)
    //
    d_decAvailableGoals = d_mapStandings.get( GStandingsType.TOTAL ).getRemainingGoals();
    d_nNumRemainingTotalMatches = d_mapStandings.get( GStandingsType.TOTAL ).getNumRemainingMatches();
    d_nNumRemainingHomeMatches = d_mapStandings.get( GStandingsType.HOME ).getNumRemainingMatches();
    d_nNumRemainingAwayMatches = d_mapStandings.get( GStandingsType.AWAY ).getNumRemainingMatches();

    // Make the moves
    //
    for ( int i = 0; i < getMatches().size(); i++ )
    {
      GMatch match = getMatches().get( i );

      d_resultRecommendation = MatchResultRecommendation.NO_RECOMMENDATION_USE_DEFAULT;
      makeMoveForMatch( match, i );

      if ( i < getMatches().size() - 1 )
        updateInternalState( match, getPlacedGoals( i ) );
    }
  }

  /**
   * Calculates the number of goals to be scored by considering the actual strength (i.e.
   * the number of goals available) of the adversary. This method must only be used if
   * there is a very limited number of games left to be played.
   *
   * @param match the match for which goals should be placed
   * @param iMatch the index of the match in the list that contains all matches for which
   *          moves need to be made (d_listMatches)
   * @return <code>true</code> if this method successfully placed goals for the given
   *         match. <code>false</code> otherwise. In the latter case, goals must be placed
   *         by the caller.
   */
  private boolean makeMoveEndOfSeasonAlgorithm( final GMatch match, final int iMatch )
  {
    // If it's not the last moveday of the season, quit immediately
    //
    if ( getMovedayStatus() != MovedayStatus.LAST_OF_SEASON )
      return false;

    // If there are less than two matches to play at the given moveday or the
    // match is not exactly the one before the last, we quit as well
    //
    if ( getMatches().size() < 2 || iMatch != getMatches().size() - 2 )
      return false;

    final GMatch match1 = match;
    final GMatch match2 = getMatches().get( iMatch + 1 );
    final GStandings s = d_mapStandings.get( GStandingsType.TOTAL );
    final GStandings s1 = d_mapAdversaryStandings.get( match1.getOpponent( getClub() ) ).get( GStandingsType.TOTAL );
    final GStandings s2 = d_mapAdversaryStandings.get( match2.getOpponent( getClub() ) ).get( GStandingsType.TOTAL );

    boolean fTryToWinMatch1 = false;
    boolean fTryToWinMatch2 = false;

    // If we have enough goals to win both matches, we will make a specific move
    // for the next match
    //
    if ( getNumAvailableGoals( match, iMatch ).compareTo( s1.getRemainingGoals().add( s2.getRemainingGoals() ) ) > 0 )
      fTryToWinMatch1 = true;
    else
    {
      final int nPtsDiff1 = Math.abs( s.getNumPoints() - s1.getNumPoints() );
      final int nPtsDiff2 = Math.abs( s.getNumPoints() - s2.getNumPoints() );

      // Otherwise we check if it is more worth to win the first match (fewer
      // points diff
      // in the standings)
      //
      if ( nPtsDiff1 < nPtsDiff2 )
      {
        int nGoals = getNumAvailableGoals( match, iMatch ).divide( match1.getCostOfOneGoal( getClub() ),
            RoundingMode.DOWN ).intValue();
        int nGoalsOpp = s1.getRemainingGoals().divide( match1.getCostOfOneGoal( match1.getOpponent( getClub() ) ),
            RoundingMode.DOWN ).intValue();

        // If we have enough goals to win the first match, it's ok
        //
        if ( nGoals > nGoalsOpp )
          fTryToWinMatch1 = true;
        else
        {
          // It does not work out for the 1st match, try the 2nd
          //
          nGoals = getNumAvailableGoals( match, iMatch ).divide( match2.getCostOfOneGoal( getClub() ),
              RoundingMode.DOWN ).intValue();
          nGoalsOpp = s2.getRemainingGoals().divide( match2.getCostOfOneGoal( match2.getOpponent( getClub() ) ),
              RoundingMode.DOWN ).intValue();

          if ( nGoals > nGoalsOpp )
            fTryToWinMatch2 = true;
        }
      }
      else
      {
        int nGoals = getNumAvailableGoals( match, iMatch ).divide( match2.getCostOfOneGoal( getClub() ),
            RoundingMode.DOWN ).intValue();
        int nGoalsOpp = s2.getRemainingGoals().divide( match2.getCostOfOneGoal( match2.getOpponent( getClub() ) ),
            RoundingMode.DOWN ).intValue();

        // If it's worthier to win the 2nd match we try. If there are not enough
        // goals, we try the first one
        //
        if ( nGoals > nGoalsOpp )
          fTryToWinMatch2 = true;
        else
        {
          nGoals = getNumAvailableGoals( match, iMatch ).divide( match1.getCostOfOneGoal( getClub() ),
              RoundingMode.DOWN ).intValue();
          nGoalsOpp = s1.getRemainingGoals().divide( match1.getCostOfOneGoal( match1.getOpponent( getClub() ) ),
              RoundingMode.DOWN ).intValue();

          if ( nGoals > nGoalsOpp )
            fTryToWinMatch1 = true;
        }
      }
    }

    if ( !fTryToWinMatch1 && !fTryToWinMatch2 )
      return false;

    // Make the move for the current match
    //
    int nGoals;
    if ( fTryToWinMatch1 )
      nGoals = s1.getRemainingGoals().divide( match1.getCostOfOneGoal( match1.getOpponent( getClub() ) ), 0,
          RoundingMode.CEILING ).intValue();
    else
    {
      // if it's for the 2nd match, we calculate how many goals we need and set
      // the
      // remaining amount of goals for the next match
      //
      nGoals = s2.getRemainingGoals().divide( match2.getCostOfOneGoal( match2.getOpponent( getClub() ) ), 0,
          RoundingMode.CEILING ).intValue();

      BigDecimal decStillAvailable = getNumAvailableGoals( match, iMatch ).subtract(
          match2.getCostOfOneGoal( getClub() ).multiply( BigDecimal.valueOf( nGoals ) ) );

      nGoals = decStillAvailable.divide( match1.getCostOfOneGoal( getClub() ), 0, RoundingMode.CEILING ).intValue();
    }

    if ( nGoals > GRulesConstants.MAX_NUM_GOALS_PER_MATCH )
      nGoals = GRulesConstants.MAX_NUM_GOALS_PER_MATCH;

    placeGoals( nGoals );

    return true;
  }

  /**
   * Tries to make an ideal move for matches of the last moveday of the contest element
   * but where more matches using the same goal account may follow in subsequent rounds.
   * Therefore this method tries to make a best effort in trying to reach the next round
   * with the minimally necessary amount of goals.
   *
   * @param match the match for which goals should be placed
   * @param iMatch the index of the match in the list that contains all matches for which
   *          moves need to be made (d_listMatches)
   * @return <code>true</code> if this method successfully placed goals for the given
   *         match. <code>false</code> otherwise. In the latter case, goals must be placed
   *         by the caller.
   */
  private boolean makeMoveEndOfContestElementAlgorithm( final GMatch match, final int iMatch )
  {
    // By default we do nothing (not required for normal league-style RR rounds)
    //
    return false;
  }

  /**
   * Places goals for the given match using the standard algorithm which treats every
   * match one by one and does not consider more than one match at a time. It is expected
   * that the internal state variables giving information about the goals account and the
   * remaining number of matches to be played are up-to-date. This method does not update
   * them at the end of the move.
   *
   * @param match the match for which to make the move
   * @param iMatch the index of the match in the list that contains all matches for which
   *          moves need to be made (d_listMatches)
   */
  protected void makeMoveForMatch( final GMatch match, final int iMatch )
  {
    // If we do not have enough goals left, then we are quickly done
    //
    if ( getNumAvailableGoals( match, iMatch ).compareTo( match.getCostOfOneGoal( getClub() ) ) <= 0 )
    {
      placeGoals( getAmountToSetWhenGoalsAccountIsEmpty( match, iMatch ) );
      return;
    }

    // Determine maximum number of goals that can be scored in the current match
    //
    d_nMaxGoalsForCurrentMatch = getNumMaxPossibleGoals( match, getClub(),
        getNumAvailableGoals( match, iMatch ).doubleValue() );

    // If it's the last match, then we place as many goals as we can
    //
    if ( getMovedayStatus() == MovedayStatus.LAST_OF_SEASON && iMatch == getMatches().size() - 1 )
    {
      placeGoals( d_nMaxGoalsForCurrentMatch );
      return;
    }

    // Check if the special end of season algorithm gives a result
    //
    if ( makeMoveEndOfSeasonAlgorithm( match, iMatch ) )
      return;

    // Check if the special end of contest element algorithm gives a result
    //
    if ( makeMoveEndOfContestElementAlgorithm( match, iMatch ) )
      return;

    // Instantiate probability array
    //
    d_anGoalProbabilities = new int[d_nMaxGoalsForCurrentMatch + 1];

    // Initialize probability array depending on whether it should be tried to
    // win the match or not
    //
    if ( wantsToWin( match ) )
      prepareBestPossibleMovetoWin( match );
    else
      prepareOptimalMoveForExpectedLoss( match );

    placeGoals( match, getClub(), d_anGoalProbabilities, d_nMaxGoalsForCurrentMatch );
  }

  /**
   * Updates the internal state member variables so that the passed match for which goals
   * have previously been placed is considered as played.
   *
   * @param match a match for which goals have been placed
   * @param nGoals the number of placed goals
   */
  private void updateInternalState( final GMatch match, final int nGoals )
  {
    d_decAvailableGoals = d_decAvailableGoals.subtract( match.getCostOfOneGoal( getClub() ).multiply(
        BigDecimal.valueOf( nGoals ) ) );

    d_nNumRemainingTotalMatches--;

    if ( match.getMatchType( getClub() ) == GMatchType.HOME )
      d_nNumRemainingHomeMatches--;
    else
      d_nNumRemainingAwayMatches--;
  }

  /**
   * Initializes the probability array d_anGoalProbabilities in such a way, that the
   * expected value of goals is very likely to produce a positive (victory) match result.
   *
   * @param match the match for which to prepare the array
   */
  private void prepareBestPossibleMovetoWin( final GMatch match )
  {
    final GStandings sTotalAdv = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get(
        GStandingsType.TOTAL );
    final GStandings sHomeAdv = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get( GStandingsType.HOME );
    final GStandings sAwayAdv = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get( GStandingsType.AWAY );
    final double dMatchImportance = getMatchImportanceFactor( match );

    // Initialize array with own goals avg.
    //
    manipulateProbabilitiesGauss(
        d_anGoalProbabilities,
        d_mapStandings.get( GStandingsType.TOTAL ).getRemainingGoals().doubleValue() / getRemainingMatchesToWin( match ),
        2.0, 2.0 );

    // Add the influence of the adversary's goals avg.
    //
    manipulateProbabilitiesGauss( d_anGoalProbabilities, sTotalAdv.getRemainingGoals().doubleValue()
        / getRemainingMatchesToWinOpponent( match ), 2.0, 1.5 + dMatchImportance / 2.0 );

    // Add the influence of the adversary's home/away balance if at least 4 home
    // or away matches have been played so far by the opponent
    //
    GStandings sAdv;
    if ( match.getMatchType( getClub() ) == GMatchType.HOME )
      sAdv = sAwayAdv;
    else
      sAdv = sHomeAdv;

    if ( sAdv.getNumMatches() > 3 )
      manipulateProbabilitiesGauss( d_anGoalProbabilities, sAdv.getGoalsPerMatchAvg(), 2.0,
          1.5 + dMatchImportance / 2.0 );
  }

  /**
   * Initializes the probability array d_anGoalProbabilities in such a way that there is
   * quite high chance to score 0 or 1 goals in the match.
   *
   * @param match the match for which to prepare the array
   */
  private void prepareOptimalMoveForExpectedLoss( final GMatch match )
  {
    // we create a gaussian distribution with an expected value of 0, the std.
    // deviation is adapted according to the match's importance
    //
    manipulateProbabilitiesGauss( d_anGoalProbabilities, 0, getMatchImportanceFactor( match ), 1 );
  }

  /**
   * Determines whether the goals placing process should make such a move that it is
   * realistic to win the match. This method should not be called if only a few matches
   * are left (special handling of last moveday's matches provide a better algorithm for
   * determining how many goals should be placed).
   *
   * The algorithm implemented by this method does the following:
   *
   * 1) If it's a home match, return <code>true</code>.
   *
   * 2) If the points difference between both teams is inferior to 4 and less than 7
   * matches are left to be played, return <code>true</code>.
   *
   * 3) If the average number of goals difference necessary to win all outstanding home
   * and half of all outstanding away matches is greater than 1, return <code>true</code>.
   *
   * 4) Otherwise there is a chance of 1/3 depending on the number of outstanding away
   * matches that <code>true</code> is returned.
   *
   * @param match the match for which it should be determined whether it should be tried
   *          to win it or not
   * @return <code>true</code> if the match should be won if possible
   */
  protected boolean wantsToWin( final GMatch match )
  {
    if ( d_resultRecommendation == MatchResultRecommendation.TRY_TO_WIN )
      return true;
    else if ( d_resultRecommendation == MatchResultRecommendation.MAY_LOSE )
      return false;

    // If the match is a home match, it should be tried to win it
    //
    if ( match.getMatchType( getClub() ) == GMatchType.HOME )
      return true;

    final GStandings sTotal = d_mapStandings.get( GStandingsType.TOTAL );
    final GStandings sTotalAdv = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get(
        GStandingsType.TOTAL );

    // If the points difference between both teams is less than 4, less than
    // 8 matches are remaining to be played and at least 4 matches have already been
    // played, the match should also be tried to be won
    //
    if ( sTotal.getNumMatches() > 3 && sTotal.getNumRemainingMatches() < 8
        && Math.abs( sTotal.getNumPoints() - sTotalAdv.getNumPoints() ) < 4 )
      return true;

    // If the average number of goals (considering that the club wants to win
    // all home and half of its away matches) is considerably higher than the one of the
    // adversary, we also want to win
    //
    final GStandings sAway = d_mapStandings.get( GStandingsType.AWAY );
    final double dGoalsAvg = d_decAvailableGoals.doubleValue() / getRemainingMatchesToWin( match );
    final double dGoalsAvgAdv = sTotalAdv.getRemainingGoals().doubleValue() / getRemainingMatchesToWinOpponent( match );

    // if there is a difference of more than 1 goal, we go ahead
    //
    if ( dGoalsAvg - dGoalsAvgAdv > 1 )
      return true;

    // In all other cases, we use a random generator by assuming that a club
    // wants to try to win 1/3 of his away matches (the probability to win will be 1/3)
    //
    int[] anP = new int[2];
    anP[0] = sAway.getNumRemainingMatches();
    anP[1] = anP[0] + sAway.getNumRemainingMatches() * 2;

    final int nRandomNumber = new Random().nextInt( anP[1] );

    return nRandomNumber <= anP[0];
  }

  /**
   * Calculates a factor between 1 (standard importance) and 3 (very important) indicating
   * how important it is to win the match. The factor is calculated based on the
   * difference of points separating both implicated clubs on the current ladder.
   *
   * @param match the match whose importance should be determined.
   * @return the match importance factor between 1 and 3
   */
  private double getMatchImportanceFactor( final GMatch match )
  {
    final double dMin = 1.0;
    final GStandings sTotal = d_mapStandings.get( GStandingsType.TOTAL );

    // The factor is only different from 1 if at least 1/3 of the contest
    // element has been played
    //
    if ( sTotal.getNumMatches() < getElement().getNumMatchesPerClub() / 3 )
      return dMin;

    final double dMax = 3.0;
    final double dStep = ( dMax - dMin ) / 16.0;
    final GStandings sTotalAdv = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get(
        GStandingsType.TOTAL );
    final int dPointsDiff = Math.abs( sTotal.getNumPoints() - sTotalAdv.getNumPoints() );

    if ( dPointsDiff > 15 )
      return dMin;

    return dMax - ( dPointsDiff * dStep );
  }

  /**
   * Returns the amount of goals that is still available for being placed. This method is
   * responsible for always returning the same amount of goals when asked multiple times
   * for the same match.
   *
   * @param match the match for which the goals should be placed
   * @param iMatch the index of the match in the list of matches for which moves must be
   *          calculated during this matchday
   * @return the number of goals still available to be placed
   */
  protected BigDecimal getNumAvailableGoals( final GMatch match, int iMatch )
  {
    return d_decAvailableGoals;
  }

  /**
   * Returns the number of matches that is remaining and that should be won. Assumes that
   * all home matches and half of the away matches should be won.
   *
   * @param match the match for which the move is currently calculated
   * @return the remaining number of matches to be won
   */
  protected int getRemainingMatchesToWin( final GMatch match )
  {
    return d_nNumRemainingHomeMatches + ( d_nNumRemainingAwayMatches / 2 );
  }

  /**
   * Returns the number of matches that is remaining and that should be won by the
   * opponent. Assumes that all home matches and half of the away matches should be won.
   *
   * @param match the match for which the move is currently calculated
   * @return the remaining number of matches to be won
   */
  protected int getRemainingMatchesToWinOpponent( final GMatch match )
  {
    final GStandings sHome = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get( GStandingsType.HOME );
    final GStandings sAway = d_mapAdversaryStandings.get( match.getOpponent( getClub() ) ).get( GStandingsType.AWAY );

    return sHome.getNumRemainingMatches() + ( sAway.getNumRemainingMatches() / 2 );
  }

  /**
   * Returns the number of goals to be set when the goal's account is empty.
   *
   * @param match the concerned match
   * @param iMatch the index of the concerned match in the list of matches
   * @return the amount of goals to be set when the goals account is empty
   */
  protected int getAmountToSetWhenGoalsAccountIsEmpty( final GMatch match, final int iMatch )
  {
    // We place 2 goals to be on the safe side, if it is too much it is automatically
    // reduced anyway
    //
    return 2;
  }
}
